"""
网络客户端 RPC 核心实现

date: 2025/8/27
author: SiHeng Tang
file: client_core.py
copyright(c) DFSA Eastern Software
此程序不能提供任何担保 WITHOUT WARRANTY OF ANY KIND
"""
import logging
import queue
import threading
import time
from copy import deepcopy
from json import JSONDecodeError
from typing import Any

from helpers.rrtlib import UnixSocketRRT, SerialRRT, IPv4SocketRRT

TELE_CONNECTED = 0
TELE_INITIAL_NOT_CONNECTED = 1
TELE_DISCONNECTED_AND_RETRY = 2
TELE_DIE = 3

MJ_RPC_COMMON_REQ = {
    "jsonrpc": "2.0",
    "method": str(),
    "params": list(),
    "id": int()
}

MJ_RPC_MESSAGE_REQ = {
    "jsonrpc": "2.0",
    "method": str(),
    "params": list()
}

MJ_RPC_TOPIC_UPDATE_REQ = {
    "jsonrpc": "2.0",
    "method": "api/mailbox",
    "params": str(),
    "id": int()
}

logger = logging.getLogger(__name__)


def _parse_param(param_item: str):
    """
    解析参数前缀，转换参数类型
    :param param_item: 原始参数
    :return: 如果类型转换成立，则返回转换后的值，否则返回原值
    """
    try:
        if param_item.startswith(".float"):
            return float(param_item[6:])
        elif param_item.startswith(".int"):
            return int(param_item[4:])
        else:
            return param_item

    except ValueError:
        return param_item


def _parse_command(_c: str):
    """
    询问发送的数据包类型
    :return: message=1 method=0，方法字符串，参数对象
    """
    tokens = _c.split(" ")

    if len(tokens) < 2:
        raise RuntimeError("Not a valid command")

    req_method = tokens[0]
    req_params = " ".join(tokens[1:])

    # 支持列表和 str int float 三种输入
    if " " in req_params:
        str_list_para = req_params.split(" ")
        req_params = []
        for p in str_list_para:
            req_params.append(_parse_param(p)) if bool(p) else None
    else:
        req_params = _parse_param(req_params)

    if _c.startswith(".msg"):
        return 1, req_method, req_params
    else:
        return 0, req_method, req_params


class _ThreadWebAPIClient(threading.Thread):
    """
    线程化 RPC 客户端，MJ RPC 客户端协议栈实现。
    """
    client_header = "mj_api_threaded_client_v0.1"
    auto_reconnect_interval = 2
    topic_update_interval = 0.2

    def __init__(self, auto_reconnect: bool):
        super().__init__()
        self.daemon = True

        self.tele = None

        self.auto_reconnect = auto_reconnect
        if self.auto_reconnect:
            self.remote_path: str | None = None
        self.drop_empty_list_in_result = True

        self._uid_big_int = 0
        self._last_topic_update_time = time.time()
        self._jsonrpc_in_pack = queue.Queue(maxsize=0)
        self._jsonrpc_out_pack = queue.Queue(maxsize=0)

        self._status = TELE_INITIAL_NOT_CONNECTED

    @property
    def status(self):
        """查询连接状态"""
        return self._status

    def connect_remote(self, path: str):
        """
        连接到 api 服务器
        :param path: 支持的地址模式 socket:/tmp/mj_interface serial:/dev/serial/by-id/...
        :return: 连接成功返回 True，失败返回 False
        """
        try:
            if path.startswith("socket:"):
                self.tele = UnixSocketRRT(path[7:])

            elif path.startswith("ipv4:"):
                self.tele = IPv4SocketRRT(path[5:])

            elif path.startswith("serial:"):
                self.tele = SerialRRT(path[7:])

            if self.auto_reconnect:
                self.remote_path = path

            if self.tele:
                ret = self._shake_hand()
                if ret:
                    self._status = TELE_CONNECTED
                return ret
            else:
                return False

        except ConnectionError as err:
            logger.error(f"can not connect to api server, while {str(err)}")

    def commit_a_request(self, method: str, params: Any, is_message=False):
        """
        提交一个 MJ RPC 请求，执行后立刻返回，协议栈在子线程中处理
        :param method: 远程方法，参考服务器远程方法列表，可以是一个指令，消息或订阅操作
        :param params: 参数，字符型或者参数列表，与服务器方法要求一致
        :param is_message: 消息标志，使用此标志发送不带 ID 不强制响应的消息
        :return: 消息返回 -1，其他请求返回 ID
        """
        if is_message:
            jsonrpc_req = deepcopy(MJ_RPC_MESSAGE_REQ)
            cmd_id = -1
        else:
            jsonrpc_req = deepcopy(MJ_RPC_COMMON_REQ)
            cmd_id = jsonrpc_req["id"] = self._get_message_uid()

        jsonrpc_req["method"] = method
        jsonrpc_req["params"] = params

        self._jsonrpc_out_pack.put(jsonrpc_req)

        return cmd_id

    def receive_a_result(self, blocking=False, timeout=0):
        """
        获取一个返回的响应，可能是一个指令响应或订阅消息回复，需要透过 ID 确认
        :param blocking: 阻塞模式，对应 queue 队列阻塞参数
        :param timeout: 超时，对应 queue 队列超时参数
        :return: MJ RPC 响应字典
        """
        return self._jsonrpc_in_pack.get(block=blocking, timeout=timeout)

    def run(self):
        # 验证连接，如果连接断开根据参数尝试恢复连接，从 RPC 队列获取请求调用，发送请求并等待回复
        while True:

            if (not bool(self.tele)) and self.auto_reconnect:
                self._status = TELE_DISCONNECTED_AND_RETRY
                logger.warning(f"Disconnected from server, auto reconnecting according to settings.")
                if not self.connect_remote(self.remote_path):
                    time.sleep(self.auto_reconnect_interval)
                    continue

            if not bool(self.tele):
                time.sleep(1)
                continue

            self._client_forever()

        self._status = TELE_DIE

    ######################
    # 私有方法
    ######################
    def _client_forever(self):
        listening = False
        mailbox_id = -1

        # 监听传入请求，编码发送传入指令，同时实现 RPC 话题查询
        try:
            while True:
                try:
                    jsonrpc_obj_to_send = self._jsonrpc_out_pack.get(block=True, timeout=0.5)

                except queue.Empty:
                    # 一段时间没有指令传入，自动刷新订阅消息
                    if time.time() - self._last_topic_update_time > self.topic_update_interval:
                        mailbox_id, jsonrpc_obj_to_send = self._get_topic_request()
                        listening = True

                    else:
                        continue

                else:
                    # 处理传入指令
                    if "id" in jsonrpc_obj_to_send:
                        listening = True

                    if time.time() - self._last_topic_update_time > self.topic_update_interval:
                        mailbox_id, jsonrpc_topic_req = self._get_topic_request()
                        jsonrpc_obj_to_send = [jsonrpc_obj_to_send, jsonrpc_topic_req]
                        listening = True
                self.tele.send_request_obj(jsonrpc_obj_to_send)

                if not listening:
                    continue

                jsonrpc_obj_recv = self.tele.receive_response_obj()
                listening = False

                if bool(jsonrpc_obj_recv):

                    if type(jsonrpc_obj_recv) != list:
                        jsonrpc_obj_recv = [jsonrpc_obj_recv]

                    for rpc_res in jsonrpc_obj_recv:
                        # 处理自动邮箱请求和丢弃空列表 (一般出现在空订阅回复上，比较有用)
                        if (mailbox_id > 0) and rpc_res["id"] == mailbox_id:
                            mailbox_id = -1
                        elif (self.drop_empty_list_in_result and
                              ("result" in rpc_res) and (type(rpc_res["result"]) == list) and
                              (not bool(rpc_res["result"]))):
                            pass

                        else:
                            self._jsonrpc_in_pack.put(rpc_res)

                logger.debug(f"link speed {self.tele.speed} B/s")


        except (BrokenPipeError, OSError, ConnectionError, JSONDecodeError) as err:
            logger.warning(f"data transmit terminated by {str(err)} session disconnected")

        finally:
            self.tele.close()
            self.tele = None

    def _shake_hand(self):
        """
        通过 api/echo 这一特殊 API 判断连接是否正常
        :return: 收到正确响应
        """
        rpc_json = deepcopy(MJ_RPC_COMMON_REQ)
        rpc_json["method"] = "api/echo"
        rpc_json["params"] = [f"mj_client_{self.client_header}"]
        rpc_json["id"] = self._get_message_uid()
        self.tele.send_request_obj(rpc_json)

        json_res = self.tele.receive_response_obj()
        if isinstance(json_res, list):
            json_res = json_res[0]
        if "error" in json_res:
            return False

        return json_res.get("result") == rpc_json["params"]

    def _get_message_uid(self):
        self._uid_big_int += 1
        return self._uid_big_int

    def _get_topic_request(self):
        jsonrpc_topic_req = deepcopy(MJ_RPC_TOPIC_UPDATE_REQ)
        jsonrpc_topic_req["params"] = ""
        mailbox_req_id = jsonrpc_topic_req["id"] = self._get_message_uid()
        return mailbox_req_id, jsonrpc_topic_req


MJWebAPIClient = _ThreadWebAPIClient
